/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.predic8.membrane.core.http.cookie;

import java.io.Serializable;
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;


/**
 * Server-side cookie representation.
 * Allows recycling and uses MessageBytes as low-level
 * representation ( and thus the byte-> char conversion can be delayed
 * until we know the charset ).
 *
 * Tomcat.core uses this recyclable object to represent cookies,
 * and the facade will convert it to the external representation.
 *
 * Source: http://tomcat.apache.org/
 * License:  http://www.apache.org/licenses/LICENSE-2.0
 *
 * @author unknown (Tomcat)
 */
public class ServerCookie implements Serializable {

	private static final long serialVersionUID = 1L;

	// Version 0 (Netscape) attributes
	private final MessageBytes name=MessageBytes.newInstance();
	private final MessageBytes value=MessageBytes.newInstance();
	// Expires - Not stored explicitly. Generated from Max-Age (see V1)
	private final MessageBytes path=MessageBytes.newInstance();
	private final MessageBytes domain=MessageBytes.newInstance();
	private boolean secure;

	// Version 1 (RFC2109) attributes
	private final MessageBytes comment=MessageBytes.newInstance();
	private int maxAge = -1;
	private int version = 0;

	// Other fields
	private static final String OLD_COOKIE_PATTERN =
			"EEE, dd-MMM-yyyy HH:mm:ss z";
	private static final ThreadLocal<DateFormat> OLD_COOKIE_FORMAT =
			new ThreadLocal<DateFormat>() {
		@Override
		protected DateFormat initialValue() {
			DateFormat df =
					new SimpleDateFormat(OLD_COOKIE_PATTERN, Locale.US);
			df.setTimeZone(TimeZone.getTimeZone("GMT"));
			return df;
		}
	};
	private static final String ancientDate;

	static {
		ancientDate = OLD_COOKIE_FORMAT.get().format(new Date(10000));
	}

	// Note: Servlet Spec =< 3.0 only refers to Netscape and RFC2109,
	// not RFC2965

	// Version 2 (RFC2965) attributes that would need to be added to support
	// v2 cookies
	// CommentURL
	// Discard - implied by maxAge <0
	// Port

	public ServerCookie() {
		// NOOP
	}

	public void recycle() {
		path.recycle();
		name.recycle();
		value.recycle();
		comment.recycle();
		maxAge=-1;
		path.recycle();
		domain.recycle();
		version=0;
		secure=false;
	}

	public MessageBytes getComment() {
		return comment;
	}

	public MessageBytes getDomain() {
		return domain;
	}

	public void setMaxAge(int expiry) {
		maxAge = expiry;
	}

	public int getMaxAge() {
		return maxAge;
	}

	public MessageBytes getPath() {
		return path;
	}

	public void setSecure(boolean flag) {
		secure = flag;
	}

	public boolean getSecure() {
		return secure;
	}

	public MessageBytes getName() {
		return name;
	}

	public MessageBytes getValue() {
		return value;
	}

	public int getVersion() {
		return version;
	}

	public void setVersion(int v) {
		version = v;
	}


	// -------------------- utils --------------------

	@Override
	public String toString() {
		return "Cookie " + getName() + "=" + getValue() + " ; "
				+ getVersion() + " " + getPath() + " " + getDomain();
	}

	// -------------------- Cookie parsing tools


	public static void appendCookieValue( StringBuffer headerBuf,
			int version,
			String name,
			String value,
			String path,
			String domain,
			String comment,
			int maxAge,
			boolean isSecure,
			boolean isHttpOnly)
	{
		StringBuffer buf = new StringBuffer();
		// Servlet implementation checks name
		buf.append( name );
		buf.append("=");
		// Servlet implementation does not check anything else

		/*
		 * The spec allows some latitude on when to send the version attribute
		 * with a Set-Cookie header. To be nice to clients, we'll make sure the
		 * version attribute is first. That means checking the various things
		 * that can cause us to switch to a v1 cookie first.
		 *
		 * Note that by checking for tokens we will also throw an exception if a
		 * control character is encountered.
		 */
		// Start by using the version we were asked for
		int newVersion = version;

		// If it is v0, check if we need to switch
		if (newVersion == 0 &&
				(!CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 &&
						CookieSupport.isHttpToken(value) ||
						CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 &&
						CookieSupport.isV0Token(value))) {
			// HTTP token in value - need to use v1
			newVersion = 1;
		}

		if (newVersion == 0 && comment != null) {
			// Using a comment makes it a v1 cookie
			newVersion = 1;
		}

		if (newVersion == 0 &&
				(!CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 &&
						CookieSupport.isHttpToken(path) ||
						CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 &&
						CookieSupport.isV0Token(path))) {
			// HTTP token in path - need to use v1
			newVersion = 1;
		}

		if (newVersion == 0 &&
				(!CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 &&
						CookieSupport.isHttpToken(domain) ||
						CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 &&
						CookieSupport.isV0Token(domain))) {
			// HTTP token in domain - need to use v1
			newVersion = 1;
		}

		// Now build the cookie header
		// Value
		maybeQuote(buf, value);
		// Add version 1 specific information
		if (newVersion == 1) {
			// Version=1 ... required
			buf.append ("; Version=1");

			// Comment=comment
			if ( comment!=null ) {
				buf.append ("; Comment=");
				maybeQuote(buf, comment);
			}
		}

		// Add domain information, if present
		if (domain!=null) {
			buf.append("; Domain=");
			maybeQuote(buf, domain);
		}

		// Max-Age=secs ... or use old "Expires" format
		if (maxAge >= 0) {
			if (newVersion > 0) {
				buf.append ("; Max-Age=");
				buf.append (maxAge);
			}
			// IE6, IE7 and possibly other browsers don't understand Max-Age.
			// They do understand Expires, even with V1 cookies!
			if (newVersion == 0 || CookieSupport.ALWAYS_ADD_EXPIRES) {
				// Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format )
				buf.append ("; Expires=");
				// To expire immediately we need to set the time in past
				if (maxAge == 0) {
					buf.append( ancientDate );
				} else {
					OLD_COOKIE_FORMAT.get().format(
							new Date(System.currentTimeMillis() +
									maxAge*1000L),
									buf, new FieldPosition(0));
				}
			}
		}

		// Path=path
		if (path!=null) {
			buf.append ("; Path=");
			maybeQuote(buf, path);
		}

		// Secure
		if (isSecure) {
			buf.append ("; Secure");
		}

		// HttpOnly
		if (isHttpOnly) {
			buf.append("; HttpOnly");
		}
		headerBuf.append(buf);
	}

	/**
	 * Quotes values if required.
	 * @param buf
	 * @param value
	 */
	private static void maybeQuote (StringBuffer buf, String value) {
		if (value==null || value.length()==0) {
			buf.append("\"\"");
		} else if (CookieSupport.alreadyQuoted(value)) {
			buf.append('"');
			buf.append(escapeDoubleQuotes(value,1,value.length()-1));
			buf.append('"');
		} else if (CookieSupport.isHttpToken(value) &&
				!CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 ||
				CookieSupport.isV0Token(value) &&
				CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0) {
			buf.append('"');
			buf.append(escapeDoubleQuotes(value,0,value.length()));
			buf.append('"');
		} else {
			buf.append(value);
		}
	}


	/**
	 * Escapes any double quotes in the given string.
	 *
	 * @param s the input string
	 * @param beginIndex start index inclusive
	 * @param endIndex exclusive
	 * @return The (possibly) escaped string
	 */
	private static String escapeDoubleQuotes(String s, int beginIndex, int endIndex) {

		if (s == null || s.length() == 0 || s.indexOf('"') == -1) {
			return s;
		}

		StringBuffer b = new StringBuffer();
		for (int i = beginIndex; i < endIndex; i++) {
			char c = s.charAt(i);
			if (c == '\\' ) {
				b.append(c);
				//ignore the character after an escape, just append it
				if (++i>=endIndex) {
					throw new IllegalArgumentException("Invalid escape character in cookie value.");
				}
				b.append(s.charAt(i));
			} else if (c == '"') {
				b.append('\\').append('"');
			} else {
				b.append(c);
			}
		}

		return b.toString();
	}

}

